/*
 * GNU AGPL-3.0 License
 *
 * Copyright (c) 2021 - present core.ai . All rights reserved.
 * Original work Copyright (c) 2015 - 2021 Adobe Systems Incorporated. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
 *
 */

// @INCLUDE_IN_API_DOCS


define(function (require, exports, module) {


    // Load dependencies.
    var TokenUtils  = require("utils/TokenUtils");

    /**
     * Enum token tag
     * @type {number}
     */
    const TOKEN_TAG = 1;

    /**
     * Enum token attribute
     * @type {number}
     */
    const TOKEN_ATTR = 2;

    /**
     * Enum token value
     * @type {number}
     */
    const TOKEN_VALUE = 3;


    /**
     * Regex to find whitespace
     * @type {RegExp}
     */
    var regexWhitespace = /^\s+$/;

    /**
     * Returns an object that represents all its params.
     * @private
     * @param {!Token} token CodeMirror token at the current pos
     * @param {number} tokenType Type of current token
     * @param {number} offset Offset in current token
     * @param {Array.<string>} exclusionList List of attributes of a tag or attribute options used by an attribute
     * @param {string} tagName Name of the current tag
     * @param {string} attrName Name of the current attribute
     * @param {boolean} shouldReplace true if we don't want to append ="" to an attribute
     * @return {!{token: Token, tokenType: int, offset: int, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
     */
    function _createTagInfo(token, tokenType, offset, exclusionList, tagName, attrName, shouldReplace) {
        return {
            token: token || null,
            tokenType: tokenType || null,
            offset: offset || 0,
            exclusionList: exclusionList || [],
            tagName: tagName || "",
            attrName: attrName || "",
            shouldReplace: shouldReplace || false
        };
    }

    /**
     * Return the tagName and a list of attributes used by the tag.
     * @private
     * @param {!Editor} editor An instance of active editor
     * @param {!{line: number, ch: number}} constPos The position of cursor in the active editor
     * @return {!{tagName: string, exclusionList: Array.<string>, shouldReplace: boolean}}
     */
    function _getTagAttributes(editor, constPos) {
        var pos, ctx, ctxPrev, ctxNext, ctxTemp, tagName, exclusionList = [], shouldReplace;

        pos = $.extend({}, constPos);
        ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);

        // Stop if the cursor is before = or an attribute value.
        ctxTemp = $.extend(true, {}, ctx);
        if (ctxTemp.token.type === null && regexWhitespace.test(ctxTemp.token.string)) {
            if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctxTemp)) {
                if ((ctxTemp.token.type === null && ctxTemp.token.string === "=") ||
                        ctxTemp.token.type === "string") {
                    return null;
                }
                TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctxTemp);
            }
        }

        // Incase an attribute is followed by an equal sign, shouldReplace will be used
        // to prevent from appending ="" again.
        if (ctxTemp.token.type === "attribute") {
            if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctxTemp)) {
                if (ctxTemp.token.type === null && ctxTemp.token.string === "=") {
                    shouldReplace = true;
                }
            }
        }

        // Look-Back and get the attributes and tag name.
        pos = $.extend({}, constPos);
        ctxPrev = TokenUtils.getInitialContext(editor._codeMirror, pos);
        while (TokenUtils.movePrevToken(ctxPrev)) {
            if (ctxPrev.token.type && ctxPrev.token.type.indexOf("tag bracket") >= 0) {
                // Disallow hints in closed tag and inside tag content
                if (ctxPrev.token.string === "</" || ctxPrev.token.string.indexOf(">") !== -1) {
                    return null;
                }
            }

            // Get attributes.
            if (ctxPrev.token.type === "attribute") {
                exclusionList.push(ctxPrev.token.string);
            }

            // Get tag.
            if (ctxPrev.token.type === "tag") {
                tagName = ctxPrev.token.string;
                if (TokenUtils.movePrevToken(ctxPrev)) {
                    if (ctxPrev.token.type === "tag bracket" && ctxPrev.token.string === "<") {
                        break;
                    }
                    return null;
                }
            }
        }

        // Look-Ahead and find rest of the attributes.
        pos = $.extend({}, constPos);
        ctxNext = TokenUtils.getInitialContext(editor._codeMirror, pos);
        while (TokenUtils.moveNextToken(ctxNext)) {
            if (ctxNext.token.type === "string" && ctxNext.token.string === "\"") {
                return null;
            }

            // Stop on closing bracket of its own tag or opening bracket of next tag.
            if (ctxNext.token.type === "tag bracket" &&
                    (ctxNext.token.string.indexOf(">") >= 0 || ctxNext.token.string === "<")) {
                break;
            }
            if (ctxNext.token.type === "attribute" && exclusionList.indexOf(ctxNext.token.string) === -1) {
                exclusionList.push(ctxNext.token.string);
            }
        }
        return {
            tagName: tagName,
            exclusionList: exclusionList,
            shouldReplace: shouldReplace
        };
    }

    /**
     * Return the tag name, attribute name and a list of options used by the attribute
     * @private
     * @param {!Editor} editor An instance of active editor
     * @param {!{line: number, ch: number}} pos Position of cursor in the editor
     * @return {!{tagName: string, attrName: string, exclusionList: Array.<string>}}
     */
    function _getTagAttributeValue(editor, pos) {
        var ctx, tagName, attrName, exclusionList = [], offset, textBefore, textAfter;

        ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);
        offset = TokenUtils.offsetInToken(ctx);

        // To support multiple options on the same attribute, we have
        // to break the value, these values will not be available then.
        if (ctx.token.type === "string" && /\s+/.test(ctx.token.string)) {
            textBefore = ctx.token.string.substr(1, offset);
            textAfter = ctx.token.string.substr(offset);

            // Remove quote from end of the string.
            if (/^['"]$/.test(ctx.token.string.substr(-1, 1))) {
                textAfter = textAfter.substr(0, textAfter.length - 1);
            }

            // Split the text before and after the offset, skipping the current query.
            exclusionList = exclusionList.concat(textBefore.split(/\s+/).slice(0, -1));
            exclusionList = exclusionList.concat(textAfter.split(/\s+/));

            // Filter through the list removing empty strings.
            exclusionList = exclusionList.filter(function (value) {
                if (value.length > 0) {
                    return true;
                }
            });
        }

        // Look-back and find tag and attributes.
        while (TokenUtils.movePrevToken(ctx)) {
            if (ctx.token.type === "tag bracket") {
                // Disallow hints in closing tags.
                if (ctx.token.string === "</") {
                    return null;
                }
                // Stop when closing bracket of another tag or opening bracket of its own in encountered.
                if (ctx.token.string.indexOf(">") >= 0 || ctx.token.string === "<") {
                    break;
                }
            }

            // Get the first previous attribute.
            if (ctx.token.type === "attribute" && !attrName) {
                attrName = ctx.token.string;
            }

            // Stop if we get a bracket after tag.
            if (ctx.token.type === "tag") {
                tagName = ctx.token.string;
                if (TokenUtils.movePrevToken(ctx)) {
                    if (ctx.token.type === "tag bracket" && ctx.token.string === "<") {
                        break;
                    }
                    return null;
                }
            }
        }

        return {
            tagName: tagName,
            attrName: attrName,
            exclusionList: exclusionList
        };
    }

    /**
     * Return the tag info at a given position in the active editor
     *
     * @param {!Editor} editor Instance of active editor
     * @param {!{line: number, ch: number}} pos Position of cursor in the editor
     * @return {!{token: Object, tokenType: number, offset: number, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
     */
    function getTagInfo(editor, pos) {
        var ctx, offset, tagAttrs, tagAttrValue;

        ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);
        offset = TokenUtils.offsetInToken(ctx);

        if (ctx.token && ctx.token.type === "tag bracket" && ctx.token.string === "<") {
            // Returns tagInfo when an angle bracket is created.
            return _createTagInfo(ctx.token, TOKEN_TAG);
        } else if (ctx.token && ctx.token.type === "tag") {
            // Return tagInfo when a tag is created.
            if (TokenUtils.movePrevToken(ctx)) {
                if (ctx.token.type === "tag bracket" && ctx.token.string === "<") {
                    TokenUtils.moveNextToken(ctx);
                    return _createTagInfo(ctx.token, TOKEN_TAG, offset);
                }
            }
        } else if (ctx.token && (ctx.token.type === "attribute" ||
                                 (ctx.token.type === null && regexWhitespace.test(ctx.token.string)))) {
            // Return tagInfo when an attribute is created.
            tagAttrs = _getTagAttributes(editor, pos);
            if (tagAttrs && tagAttrs.tagName) {
                return _createTagInfo(ctx.token, TOKEN_ATTR, offset, tagAttrs.exclusionList, tagAttrs.tagName, null, tagAttrs.shouldReplace);
            }
        } else if (ctx.token && ((ctx.token.type === null && ctx.token.string === "=") ||
                                 (ctx.token.type === "string" && /^['"]$/.test(ctx.token.string.charAt(0))))) {
            // Return tag info when an attribute value is created.
            // Allow no hints if the cursor is outside the value.
            if (ctx.token.type === "string" &&
                    /^['"]$/.test(ctx.token.string.substr(-1, 1)) &&
                    ctx.token.string.length !== 1 &&
                    ctx.token.end === pos.ch) {
                return _createTagInfo();
            }

            tagAttrValue = _getTagAttributeValue(editor, pos);
            if (tagAttrValue && tagAttrValue.tagName && tagAttrValue.attrName) {
                return _createTagInfo(ctx.token, TOKEN_VALUE, offset, tagAttrValue.exclusionList, tagAttrValue.tagName, tagAttrValue.attrName);
            }
        }
        return _createTagInfo();
    }

    /**
     * Return the query text of a value.
     *
     * @param {!{token: Object, tokenType: number, offset: number, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
     * @return {string}  The query to use to matching hints.
     */
    function getValueQuery(tagInfo) {
        var query;
        if (tagInfo.token.string === "=") {
            return "";
        }
        // Remove quotation marks in query.
        query = tagInfo.token.string.substr(1, tagInfo.offset - 1);

        // Get the last option to use as a query to support multiple options.
        return query.split(/\s+/).slice(-1)[0];
    }

    // Expose public API.
    exports.getTagInfo      = getTagInfo;
    exports.getValueQuery   = getValueQuery;
    exports.regexWhitespace = regexWhitespace;
    exports.TOKEN_TAG       = TOKEN_TAG;
    exports.TOKEN_ATTR      = TOKEN_ATTR;
    exports.TOKEN_VALUE     = TOKEN_VALUE;
});
